﻿using CleanArchitecture.Core;
using CleanArchitecture.Core.Handlers;
using CleanArchitecture.Infrastructure.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

namespace CleanArchitecture.Infrastructure.ModelBinding
{
    /// <summary>
    /// Represents model binder provider for the creating Custom ModelBinder
    /// </summary>
    public class MvcModelBinderProvider : IModelBinderProvider
    {
        private readonly IList<IInputFormatter> _formatters;
        private readonly IHttpRequestStreamReaderFactory _readerFactory;
        private readonly ILoggerFactory _loggerFact;
        /// <summary>
        /// Creates a new <see cref="BodyModelBinderProvider"/>.
        /// </summary>
        /// <param name="formatters">The list of <see cref="IInputFormatter"/>.</param>
        /// <param name="readerFactory">The <see cref="IHttpRequestStreamReaderFactory"/>.</param>
        public MvcModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
        {
            this._formatters = formatters;
            this._readerFactory = readerFactory;
            this._loggerFact = EngineContext.Current.Resolve<ILoggerFactory>();
            if (_formatters == null)
            {
                throw new ArgumentNullException(nameof(_formatters));
            }

            if (_readerFactory == null)
            {
                throw new ArgumentNullException(nameof(_readerFactory));
            }
        }
        /// <summary>
        /// Creates a nop model binder based on passed context
        /// DavidLee 2019/11/16 
        //  For security reasons, in particular cross-site request forgery (CSRF)) prevention...
        //  dotnet Core prevented ModelBinding for ajax(JSON POSTs) way by default!
        //  see : https://andrewlock.net/model-binding-json-posts-in-asp-net-core/
        /// 由于.net core基于安全考虑默为不提供前端json post至Controller/action中的模型参数绑定，需要在action方法中
        /// 的模型参数前加上[FromBody]属性以从Http Request中从Request.Body绑定Request Json Values到参数模型中
        /// 例：IActionResult CheckLogin([FromBody]LoginModel model)
        /// 通过资料查询和.net core源码找到处理方法满足前端ajax提交数据至action方法完成数据模型绑定
        /// 
        /// see The [FromBody] on my action method parameter is causing the model to be bound from the JSON payload that is posted to the endpoint
        /// see https://stackoverflow.com/questions/45495432/asp-net-core-mvc-mixed-route-frombody-model-binding-validation
        /// see https://stackoverflow.com/questions/45495432/asp-net-core-mvc-mixed-route-frombody-model-binding-validation?rq=1
        /// </summary>
        /// <param name="context">Model binder provider context</param>
        /// <returns>Model binder</returns>
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
                throw new ArgumentNullException(nameof(context));


            var modelType = context.Metadata.ModelType;
            if (!typeof(BaseEntityModel).IsAssignableFrom(modelType))
                return null;

            //use NopModelBinder as a ComplexTypeModelBinder for BaseNopModel
            if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
            {
                BodyModelBinder bodymodelBinder = null;
                ComplexTypeModelBinder complexmodelBinder = null;
                if (context.BindingInfo.BindingSource != null &&
                context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body))
                {
                    if (_formatters.Count == 0)
                    {
                        throw new NowaException("MvcModelBinderProvider:FormatInputFormattersAreRequired for {0}, {1}, {2}",
                            typeof(MvcOptions).FullName,
                            nameof(MvcOptions.InputFormatters),
                            typeof(IInputFormatter).FullName);
                    }
                    bodymodelBinder = new BodyModelBinder(this._formatters, this._readerFactory);
                }
                //create binders for all model properties
                var propertyBinders = context.Metadata.Properties
                    .ToDictionary(modelProperty => modelProperty, modelProperty => context.CreateBinder(modelProperty));

                complexmodelBinder = new ComplexTypeModelBinder(propertyBinders, this._loggerFact);
                return new MvcModelBinder(complexmodelBinder, bodymodelBinder);
            }

            //or return null to further search for a suitable binder
            return null;
        }
    }
}
